const fs = require('fs');
const pathUtil = require('path');
const crypto = require('crypto');
const props = require('define-props');

const md5 = (...segments) => {
	const str = segments.join('');
	if (str === '') return '';
	return crypto
		.createHash('md5')
		.update(str.toString())
		.digest('hex');
};

const DEV = 'development';
const TEST = 'test';
const PROD = 'production';

const EnvAliases = {
	[DEV]: DEV,
	[TEST]: TEST,
	[PROD]: PROD,
	dev: DEV,
	devel: DEV,
	prod: PROD,
	0: DEV,
	1: TEST,
	2: PROD,
};

let EnvIndex = 0;

class AppConf {
	/**
	 * 项目配置实例的hash，每个AppConf实例都有自己唯一的hash
	 *
	 * @memberof AppConf
	 */
	// hash = '';

	/**
	 * 执行时process.cwd()的目录值，一般为node.js项目的项目根目录(并不是执行的脚本文件所在的目录)
	 *
	 * @memberof AppConf
	 */
	// cwd = '';

	/**
	 * 当前项目配置实例的项目根目录，可在实例化时指定
	 *
	 * @memberof AppConf
	 */
	// root = '';

	/**
	 * 当前OS环境下的userhome目录
	 *
	 * @memberof AppConf
	 */
	// userHome = '';

	/**
	 * 是否windows环境
	 *
	 * @memberof AppConf
	 */
	// isWin = false;

	/**
	 * 当前项目的运行环境标识文件，该文件应该放到版本忽略文件中，如： .gitignore
	 *
	 * @memberof AppConf
	 */
	// envFile = '.appenv';

	/**
	 * 当前项目的package信息文件，一般默认为package.json
	 *
	 * @memberof AppConf
	 */
	// packageFile = 'package.json';

	/**
	 * 项目实例的环境值
	 *
	 * 这里只是 class 代码提示的预占位，实际运行时为 getter
	 *
	 * @memberof AppConf
	 */
	// env = '';

	/**
	 * 项目的package信息内容
	 *
	 * 这里只是 class 代码提示的预占位，实际运行时为 getter
	 *
	 * @memberof AppConf
	 */
	// packageInfo = {
	// 	name: '',
	// };

	/**
	 * 项目的配置目录名
	 *
	 * @memberof AppConf
	 */
	// configDir = 'config';

	/**
	 * 项目的配置文件名
	 *
	 * @memberof AppConf
	 */
	// configFile = 'config';

	/**
	 * 项目配置文件的后缀文件名，如果非 json 格式，请自行在继承类重载 loadConfig 方法
	 *
	 * @memberof AppConf
	 */
	// configExt = 'json';

	/**
	 * 项目的配置数据
	 *
	 * @memberof AppConf
	 */
	// config = {};

	/**
	 * Creates an instance of AppConf.
	 *
	 * @param {string} root 当前 AppConf 实例项目根目录，如果目录不存在，则以 process.cwd() 来填充
	 * @param {any} env 当前 AppConf 实例的环境名，主要用于指定特定环境名，主要用于测试，一般使用可不指定，而根据 envFile 来读取
	 * @param {string} file 强制当前 AppConf 实例只加载指定的配置文件，主要用于测试，一般使用可不指定
	 * @memberof AppConf
	 */
	constructor(root, env, file) {
		// 属性预定义，便于编辑器识别自动代码
		this.hash = '';
		this.cwd = '';
		this.root = '';
		this.userHome = '';
		this.isWin = false;
		this.env = '';
		this.envFile = '.appenv';
		this.packageFile = 'package.json';
		this.packageInfo = {
			name: '',
		};
		this.configDir = 'config';
		this.configFile = 'config';
		this.configExt = 'json';
		this.config = {};

		const cwd = process.cwd();
		const isWin = process.platform === 'win32';
		const userHome = process.env[isWin ? 'USERPROFILE' : 'HOME'];
		try {
			if (typeof root !== 'string' || root === null || root === '' || !fs.lstatSync(root).isDirectory())
				root = cwd;
		} catch (err) {
			root = cwd;
		}
		const date = new Date();
		const hash = md5(root, date.valueOf(), EnvIndex++);

		let appEnv, packageInfo, config;
		props(this, {
			hash,
			cwd,
			root,
			userHome,
			isWin,
			env: {
				get: () => {
					if (typeof appEnv === 'undefined') {
						appEnv = this.loadAppEnv(env);
					}
					return appEnv;
				},
			},
			packageInfo: {
				get: () => {
					if (typeof packageInfo === 'undefined') {
						packageInfo = this.loadPackageInfo();
					}
					return packageInfo;
				},
			},
			config: {
				get: () => {
					if (typeof config === 'undefined') {
						config = this.loadConfig(file);
					}
					return config;
				},
			},
		});
	}

	loadFileSync(path, dft = '') {
		let text = '';
		if (fs.existsSync(path)) {
			try {
				const buff = fs.readFileSync(path);
				text = buff.toString().trim();
			} catch (err) {}
		}
		if (text === '') text = dft;
		return text;
	}

	loadJsonSync(path, dft) {
		try {
			const text = this.loadFileSync(path);
			if (text !== '') return JSON.parse(text);
		} catch (err) {}
		return dft;
	}

	loadAppEnv(env) {
		if (typeof env === 'undefined' || env === null) {
			try {
				env = this.loadFileSync(this.path(this.envFile), DEV);
			} catch (err) {}
		}
		return EnvAliases[env] ? EnvAliases[env] : DEV;
	}

	loadPackageInfo() {
		let info = {};
		try {
			info = this.loadJsonSync(this.path(this.packageFile), {});
		} catch (err) {}
		if (typeof info !== 'object' || Array.isArray(info)) info = {};
		if (typeof info.name !== 'string' || info.name.trim() === '') info.name = pathUtil.basename(this.root);
		return info;
	}

	appName() {
		const info = this.packageInfo;
		const name =
			typeof info.name !== 'string' || info.name.trim() === '' ? pathUtil.basename(this.root) : info.name.trim();
		return name;
	}

	path(path) {
		return pathUtil.resolve(this.root, path);
	}

	getConfigFiles() {
		return [
			// app/config/development.json
			this.path(`${this.configDir}/${this.env}.${this.configExt}`),
			// app/config/config.json
			this.path(`${this.configDir}/${this.configFile}.${this.configExt}`),
			// app/config.json
			this.path(`${this.configFile}.${this.configExt}`),
		];
	}

	selectFiles(files, then) {
		if (Array.isArray(files)) {
			for (let i = 0, size = files.length; i < size; i++) {
				let file = files[i];
				if (fs.existsSync(file)) {
					if (typeof then === 'function') then(file);
					return file;
				}
			}
		}
		return false;
	}

	loadConfig(file) {
		if (typeof file === 'undefined' || file === null) file = this.selectFiles(this.getConfigFiles());
		if (file === false) throw new Error('No config file detect!');
		let config = this.loadJsonSync(file, {});
		config = config[this.env] || config || {};
		if (typeof config !== 'object' || Array.isArray(config)) config = {};
		return config;
	}
}

module.exports = AppConf;
